//==========================================================================
// Copyright (c) 2000-2008,  Elastos, Inc.  All Rights Reserved.
//==========================================================================

#include <errno.h>
#include <define.h>
#include <serialio.h>
#include <control.h>
#include "lh7a400_serial.h"

typedef struct lh7a400_serial_info {
    SERIAL_ADDRWORD   base;
    SERIAL_WORD       int_num;
} lh7a400_serial_info;

static bool lh7a400_serial_init(struct devtab_entry *tab);
static SERIAL_ErrNo lh7a400_serial_lookup(struct devtab_entry **tab,
                                       struct devtab_entry *sub_tab,
                                       const char *name);

static bool lh7a400_serial_open(serial_channel *chan);
static bool lh7a400_serial_close(serial_channel *chan);

static bool lh7a400_serial_putc(serial_channel *chan, unsigned char c);
static bool lh7a400_serial_getc(serial_channel *chan, unsigned char *c);
static SERIAL_ErrNo lh7a400_serial_set_config(serial_channel *chan,
                                           Int32 key,
                                           const void *xbuf, Int32 *len);
static void lh7a400_serial_start_xmit(serial_channel *chan);
static void lh7a400_serial_stop_xmit(serial_channel *chan);

static int lh7a400_serial_ISR(serial_channel *chan);

static SERIAL_FUNS(
    lh7a400_serial_funs,
    lh7a400_serial_open,
    lh7a400_serial_close,
    lh7a400_serial_putc,
    lh7a400_serial_getc,
    lh7a400_serial_set_config,
    lh7a400_serial_start_xmit,
    lh7a400_serial_stop_xmit,
    lh7a400_serial_ISR
);


// config special serial port

port_tbl_t g_port_tbl = {
    3,
    {
        {AAEC_UART1, AAECNUM_HAL_INTERRUPT_UART1INTR},
        {AAEC_UART2, AAECNUM_HAL_INTERRUPT_UART2INTR},
        {AAEC_UART3, AAECNUM_HAL_INTERRUPT_UART3INTR},
    },
};

bool serial_devtab_res_init(devtab_entry * tab,
                        unsigned int port_base,
                        int irq)
{
    assert(tab);
    serial_channel *channel = new serial_channel;
    if (!channel) { return false; }

    lh7a400_serial_info *info = new lh7a400_serial_info;
    if (!info) { return false; }

    info->base = port_base;
    info->int_num = irq;

    channel->dev_priv = info;

    tab->handlers = &g_serial_devio;
    tab->init = lh7a400_serial_init;
    tab->lookup = lh7a400_serial_lookup;
    tab->priv = channel;

#if 0
    // max3244 power on
    SERIAL_ADDRWORD pwr_io = 0x20000000 + 4 * 3;
    UInt8 val;
    HAL_READ_UINT8(pwr_io, val);
    HAL_WRITE_UINT8(pwr_io, val | (UInt8) (1 << 5));
#endif

    tab->init(tab);

    return true;
}

// Internal function to actually configure the hardware to desired
// baud rate, etc.
static bool
lh7a400_serial_config_port(serial_channel *chan,
                           SERIAL_INFO *new_config,
                           bool init)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;

    unsigned short baud_divisor = select_baud[new_config->baud];
    UInt32 _lcr, _intm;
    if (baud_divisor == 0) { return false; }

    // Register writes don't take effect till the UART is enabled.
    HAL_WRITE_UINT32(base + AAEC_UART_CTRL, AAEC_UART_CTRL_ENAB);

    // Disable port interrupts while changing hardware
    HAL_READ_UINT32(base+AAEC_UART_INTM, _intm);
    HAL_WRITE_UINT32(base+AAEC_UART_INTM, 0);
    HAL_WRITE_UINT32(base+AAEC_UART_INT, AAEC_UART_INT_MISEOI);

    _lcr =
        select_word_length[new_config->word_length - NUM_SERIAL_WORD_LENGTH_5] |
        select_stop_bits[new_config->stop] |
        select_parity[new_config->parity];
    _lcr |= AAEC_UART_LCR_FIFO;  // Enable FIFO

    // Set line controls
    HAL_WRITE_UINT32(base+AAEC_UART_BAUD, baud_divisor);
    HAL_WRITE_UINT32(base+AAEC_UART_LCR, _lcr);

    if (init) {
        if (chan->out_cbuf.len != 0) {
            _intm = (AAEC_UART_INT_RIS | AAEC_UART_INT_RTIS);
        }
        else {
            _intm = 0;
        }
    }

    if (chan->support_line_status) {
        _intm |= AAEC_UART_INT_MIS;
    }

    // (re)enable interrupts
    HAL_WRITE_UINT32(base+AAEC_UART_INTM, _intm);

    if (new_config != &chan->config) {
        chan->config = *new_config;
    }

    return true;
}

// Function to initialize the device.  Called at bootstrap time.
static bool
lh7a400_serial_init(struct devtab_entry *tab)
{
    serial_channel *chan = (serial_channel *)tab->priv;
    assert(chan);
    assert(chan->dev_priv);

    chan->funs = &lh7a400_serial_funs;
    chan->callbacks = &g_serial_callbacks;
    chan->config.baud =
        SERIAL_BAUD_RATE(SERIALNUM_IO_SERIAL_ARM_LH7A400_SERIAL0_BAUD);
    chan->config.stop = SERIAL_STOP_DEFAULT;
    chan->config.parity = SERIAL_PARITY_DEFAULT;
    chan->config.word_length = SERIAL_WORD_LENGTH_DEFAULT;
    chan->config.flags = SERIAL_FLAGS_DEFAULT;

    chan->support_flow_control = true;
    chan->flow_desc.flowctrl_software = false;
    chan->support_line_status = true;

    chan->bufs_init = false;

    // sub-config
#if SERIAL_ARM_LH7A400_SERIAL_OUT_BUFSIZE > 0
    chan->out_cbuf.data =
        (unsigned char *) malloc(SERIAL_ARM_LH7A400_SERIAL_OUT_BUFSIZE);
    if (!chan->out_cbuf.data) {
        return false;
    }
    memset(chan->out_cbuf.data, 0, SERIAL_ARM_LH7A400_SERIAL_OUT_BUFSIZE);
    chan->out_cbuf.put = 0;
    chan->out_cbuf.get = 0;
    chan->out_cbuf.len = SERIAL_ARM_LH7A400_SERIAL_OUT_BUFSIZE;
#endif // SERIAL_ARM_LH7A400_SERIAL_OUT_BUFSIZE

#if SERIAL_ARM_LH7A400_SERIAL_IN_BUFSIZE > 0
    chan->in_cbuf.data =
        (unsigned char *) malloc(SERIAL_ARM_LH7A400_SERIAL_IN_BUFSIZE);
    if (!chan->in_cbuf.data) {
        return false;
    }
    memset(chan->in_cbuf.data, 0, SERIAL_ARM_LH7A400_SERIAL_IN_BUFSIZE);
    chan->in_cbuf.put = 0;
    chan->in_cbuf.get = 0;
    chan->in_cbuf.len = SERIAL_ARM_LH7A400_SERIAL_IN_BUFSIZE;
#endif // SERIAL_ARM_LH7A400_SERIAL_IN_BUFSIZE

    lh7a400_serial_info *lh7a400_chan;
    lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;

#ifdef SERIALDBG_IO_INIT
    kprintf("LH7A400 SERIAL init - dev: 0x%08x.%d\n",
            lh7a400_chan->base, lh7a400_chan->int_num);
#endif

    (chan->callbacks->serial_init)(chan);  // Really only required for
                                           // interrupt driven devices

    lh7a400_serial_config_port(chan, &chan->config, true);

    return true;
}

// This routine is called when the device is "looked" up (i.e. attached)
static SERIAL_ErrNo
lh7a400_serial_lookup(struct devtab_entry **tab,
                      struct devtab_entry *sub_tab,
                      const char *name)
{
    serial_channel *chan = (serial_channel *)(*tab)->priv;
    (chan->callbacks->serial_init)(chan);  // Really only required for
                                           // interrupt driven devices
    return ENOERR;
}

static bool
lh7a400_serial_open(serial_channel *chan)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;

    HAL_WRITE_UINT32(base+AAEC_UART_INTM, chan->priv.interrupt_stat);
    chan->extension.device_opened = true;
    return true;
}

static bool
lh7a400_serial_close(serial_channel *chan)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;
    UInt32 _intm;

    HAL_READ_UINT32(base+AAEC_UART_INTM, _intm);
    chan->priv.interrupt_stat = _intm;
    HAL_WRITE_UINT32(base+AAEC_UART_INTM, 0);
    chan->extension.device_opened = false;
    return true;
}

// Send a character to the device output buffer.
// Return 'true' if character is sent to device
static bool
lh7a400_serial_putc(serial_channel *chan, unsigned char c)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;
    UInt32 _status;
    HAL_READ_UINT32(base+AAEC_UART_STATUS, _status);
    if (_status & AAEC_UART_STATUS_TxFF) {
        // No space
        return false;
    }
    else {
        // Transmit buffer is empty
        HAL_WRITE_UINT32(base+AAEC_UART_DATA, (UInt32)c);
        return true;
    }
}

// Fetch a character from the device input buffer, waiting if necessary
static bool
lh7a400_serial_getc(serial_channel *chan, unsigned char *c)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;
    UInt32 _status;
    UInt32 _c;

    HAL_READ_UINT32(base+AAEC_UART_STATUS, _status);
    if ((_status & AAEC_UART_STATUS_RxFE) == 0) {
        HAL_READ_UINT32(base+AAEC_UART_DATA, _c);
        *c = (unsigned char) _c;

        return true;
    }

    return false;
}

// Set up the device characteristics; baud rate, etc.
static SERIAL_ErrNo
lh7a400_serial_set_config(serial_channel *chan, Int32 key,
                          const void *xbuf, Int32 *len)
{
    switch (key) {
    case IO_SET_CONFIG_SERIAL_INFO:
        {
            SERIAL_INFO *config = (SERIAL_INFO *)xbuf;
            if ((UInt32)*len < sizeof(SERIAL_INFO) ) {
                return -EINVAL;
            }
            *len = sizeof(SERIAL_INFO);
            if ( true != lh7a400_serial_config_port(chan, config, false) )
                return -EINVAL;
        }
        break;

    case IO_SET_CONFIG_SERIAL_HW_RX_FLOW_THROTTLE:
        {
            // doesn't support the mode by this device
            UInt32 *f = (UInt32 *)xbuf;

            if ((UInt32)*len < sizeof(*f)) {
                return -EINVAL;
            }
            return -ENODEV;
        }
        break;

    case IO_SET_CONFIG_SERIAL_HW_FLOW_CONFIG:
        // Nothing to do because we do support both RTSCTS and DSRDTR flow
        // control.
        // Other targets would clear any unsupported flags here and
        // would then return -ENOSUPP - the higher layer can then query
        // what flags are set and decide what to do. This is optimised for
        // the most common case - i.e. that authors know what their hardware
        // is capable of.
        // We just return ENOERR.
        break;

    default:
        return -EINVAL;
    }

    return ENOERR;
}

// Enable the transmitter on the device
static void
lh7a400_serial_start_xmit(serial_channel *chan)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;
    UInt32 _intm;

    HAL_READ_UINT32(base+AAEC_UART_INTM, _intm);
    _intm |= AAEC_UART_INT_TIS;
    HAL_WRITE_UINT32(base+AAEC_UART_INTM, _intm);
}

// Disable the transmitter on the device
static void
lh7a400_serial_stop_xmit(serial_channel *chan)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;
    UInt32 _intm;

    HAL_READ_UINT32(base+AAEC_UART_INTM, _intm);
    _intm &= ~AAEC_UART_INT_TIS;
    HAL_WRITE_UINT32(base+AAEC_UART_INTM, _intm);
}

// Serial I/O - interrupt handler (ISR)
static int
lh7a400_serial_ISR(serial_channel *chan)
{
    lh7a400_serial_info *lh7a400_chan = (lh7a400_serial_info *)chan->dev_priv;
    SERIAL_ADDRWORD base = lh7a400_chan->base;
    UInt32 _intres;
    unsigned char _c;

    HAL_READ_UINT32(base + AAEC_UART_INTRES, _intres);

    while (_intres) {

        // Empty Rx FIFO
        if (_intres & (AAEC_UART_INT_RIS | AAEC_UART_INT_RTIS)) {
            while (chan->funs->getc(chan, &_c)) {
                (chan->callbacks->rcv_char)(chan, (unsigned char)_c);
            }
        }

        // Fill into Tx FIFO. xmt_char will mask the interrupt when it
        // runs out of chars, so doing this in a loop is OK.
        else if (_intres & AAEC_UART_INT_TIS) {
            (chan->callbacks->xmt_char)(chan);
        }

        else if (chan->support_line_status) {
            if (_intres & AAEC_UART_INT_MIS) {
                serial_line_status_t stat;
                UInt8 _status;

                HAL_READ_UINT32(base + AAEC_UART_STATUS, _status);

                // SERIALOPT_IO_SERIAL_FLOW_CONTROL_HW

                if (chan->config.flags & NUM_SERIAL_FLOW_DSRDTR_TX) {
                    stat.which = NUM_SERIAL_STATUS_FLOW;
                    stat.value = (0 != (_status & AAEC_UART_STATUS_DSR));
                    (chan->callbacks->indicate_status)(chan, &stat);
                }

                if (chan->config.flags & NUM_SERIAL_FLOW_RTSCTS_TX) {
                    stat.which = NUM_SERIAL_STATUS_FLOW;
                    stat.value = (0 != (_status & AAEC_UART_STATUS_CTS));
                    (chan->callbacks->indicate_status)(chan, &stat);
                }

                // others status
                {
                    stat.which = NUM_SERIAL_STATUS_CARRIERDETECT;
                    stat.value = (0 != (_status & AAEC_UART_STATUS_DCD));
                    (chan->callbacks->indicate_status)(chan, &stat);
                }

                HAL_WRITE_UINT32(base+AAEC_UART_INT, AAEC_UART_INT_MISEOI);
            }
        }

        HAL_READ_UINT32(base+AAEC_UART_INTRES, _intres);
    }

    return 0;
}

